react本身是一个UI库,它没有专门的数据获取模式,虽然说获取数据之后,我们可以使用redux这些状态管理库来处理数据在整个应用的共享,但是这些状态管理库本身对client state管理的好(比如说theme),但是对于数据获取,异步、服务器状态不是很管用。

React 工程中一个非常核心的设计点:数据获取(data fetching)和状态管理(state management)。 我们来系统地解释一下👇
🧩 一、React 自身的问题:数据获取的痛点
React 本身是一个UI 库,它不负责数据获取或缓存逻辑。当项目开始复杂化(尤其是涉及接口调用)时,就会暴露出几个典型问题。
假设你直接用
useEffect + fetch + useState来请求接口:xxxxxxxxxx181function UserList() {2const [users, setUsers] = useState<User[]>([]);3const [loading, setLoading] = useState(true);4const [error, setError] = useState<Error | null>(null);56useEffect(() => {7setLoading(true);8fetch('/api/users')9.then((res) => res.json())10.then(setUsers)11.catch(setError)12.finally(() => setLoading(false));13}, []);1415if (loading) return <p>Loading...</p>;16if (error) return <p>Error!</p>;17return <UserTable data={users} />;18}看似简单,但一旦项目复杂起来,就会出现以下问题👇:
问题 说明 🔁 重复请求 同一个接口可能在多个组件中都被请求多次 🧠 缓存难管理 要自己管理缓存、过期、刷新等逻辑 🕐 重新请求逻辑繁琐 比如窗口重新聚焦、数据变更后重新加载都要手写 ⚡ 全局状态难统一 请求状态(loading、error、data)常常散落在各处 ♻️ 刷新与同步困难 用户操作后(如新增、删除)如何让列表自动刷新? 这些都是 React 本身不擅长解决的问题。 于是,就有了 React Query(现在叫 TanStack Query)。
🚀 二、React Query 是什么?
React Query 是一个用于 数据获取(server state management) 的库。 它的核心目标是:让你不再手写 useEffect 来获取数据。
一句话总结:
🧠 React Query 让你只关心“数据是什么”,而不用关心“什么时候、怎么获取、怎么缓存”。
🎯 三、React Query 解决了哪些问题?
1️⃣ 自动管理请求的状态
你不用再写一堆
loading、error、useEffect。xxxxxxxxxx121import { useQuery } from "@tanstack/react-query";23function UserList() {4const { data, isLoading, error } = useQuery({5queryKey: ['users'],6queryFn: () => fetch('/api/users').then(res => res.json()),7});89if (isLoading) return <p>Loading...</p>;10if (error) return <p>Error!</p>;11return <UserTable data={data} />;12}React Query 自动帮你:
- 追踪 loading、error、success 状态;
- 缓存数据;
- 避免重复请求。
2️⃣ 缓存与共享
同一个
queryKey的数据会自动缓存,跨组件共享。xxxxxxxxxx11useQuery({ queryKey: ['users'], queryFn: getUsers });不论在哪个组件中调用,只要 key 一样,就共享同一份数据缓存。
3️⃣ 自动刷新与失效
- 页面重新聚焦时自动重新请求;
- 网络恢复后自动重试;
- 支持手动
invalidateQueries(['users'])来刷新数据。例如当你新增了一个用户:
xxxxxxxxxx31mutation.mutate(newUser, {2onSuccess: () => queryClient.invalidateQueries(['users'])3});➡️ 自动让
UserList重新获取最新数据!4️⃣ 并行与依赖请求
你可以轻松写出:
- 并行请求(多个
useQuery同时执行);- 依赖请求(上一个请求完成后再发下一个)。
5️⃣ SSR、分页、预取
React Query 还支持:
- SSR(服务端渲染)
- 分页、无限滚动
- 数据预取(prefetch)
- 缓存时间控制
这些都大大提升了性能与开发体验。
🧱 四、React Query vs 全局状态库(Redux、Zustand)
类型 适合管理的数据 说明 React Query 服务端状态(Server State) 从 API 获取的、可能被服务器更新的数据 Redux / Zustand / Jotai 客户端状态(Client State) 用户登录态、UI 开关、临时表单状态等 📌 一句话记住:
✅ “接口数据”用 React Query, ✅ “本地交互状态”用 Redux/Zustand。
🌍 五、React Query 的核心理念
让你的 React 应用 以声明式的方式 处理“数据获取”这件事。
你只需要告诉它:
- 你要的数据是什么(
queryKey);- 怎么获取它(
queryFn); 剩下的缓存、更新、状态、刷新,全部交给 React Query。✅ 总结
问题 React Query 的解决方式 手动写 useEffect 提供 useQuery 自动处理 重复请求 内置缓存与共享 缺乏状态管理 自动提供 isLoading/error/data 刷新困难 invalidateQueries / 自动刷新机制 性能低 缓存、请求去重、后台刷新
这里出现了client state和server state,之前没有了解过这些概念,这个概念在react中还是蛮重要的。

在 React 应用中,确实存在两种性质完全不同的状态:
👉 Client State(客户端状态) 👉 Server State(服务端状态)
它们看起来都叫 “state”,但本质、生命周期、管理方式完全不同。 我们来一步步拆开讲 👇
🧠 一、什么是 Client State(客户端状态)
Client State 指的是在浏览器或 React 应用内部自己管理的数据。 它完全存在于前端,不依赖后端。
✅ 典型例子:
- 当前页面的 UI 状态:是否展开某个 modal、当前 tab 是哪个;
- 表单输入值;
- 本地缓存的 token、用户登录状态;
- 组件之间共享的小数据;
- 临时计算结果(比如筛选条件、分页页码)。
🧩 特点:
特征 说明 💾 存在位置 浏览器内存(React 组件、Context、Redux、Zustand 等) 🔄 更新方式 前端代码直接修改 ⏳ 生命周期 跟随页面刷新而清空 🚫 不需要 API 不依赖后端 🧰 管理工具 useState / useReducer / Context / Redux / Zustand / Jotai 等 🧠 举例:
xxxxxxxxxx21const [isOpen, setIsOpen] = useState(false);2const [form, setForm] = useState({ name: '', age: '' });这些都是典型的 Client State。 前端知道怎么改,后端根本不关心。
🌍 二、什么是 Server State(服务端状态)
Server State 是从服务器获取的数据,它存在于后端系统,前端只是读取或缓存它。
✅ 典型例子:
- 从接口获取的用户信息;
- 产品列表;
- 后台返回的报表数据;
- 聊天消息、评论列表、通知等。
🧩 特点:
特征 说明 📡 来源 由服务器提供,通过 API 获取 🧠 管理难度 难(需要处理 loading、error、缓存、同步等) ♻️ 更新方式 必须请求或刷新 API 🔁 数据变化 后端更新后,前端需要重新同步 ⚙️ 管理工具 React Query / SWR / Apollo / Relay 等 🧠 举例:
xxxxxxxxxx41const { data, isLoading } = useQuery({2queryKey: ['users'],3queryFn: () => fetch('/api/users').then(res => res.json())4});这些数据不是前端控制的,而是服务器控制的。 前端只能请求 → 缓存 → 刷新。
⚖️ 三、两者的区别总结表
对比项 Client State Server State 数据来源 浏览器内存、本地逻辑 服务器、数据库 更新方式 前端逻辑直接 setState 通过 API 调用 是否可持久化 否(刷新即丢) 是(保存在后端) 是否可共享 通常只在当前页面内 多用户、多客户端共享 典型工具 useState / Redux / Zustand React Query / SWR / Apollo 主要难点 结构设计、性能优化 缓存、同步、失效、刷新 🚀 四、为什么要区分这两种状态?
在 React 早期(比如 Redux 盛行时代),很多人把两者混在一起用:
- API 请求回来的数据也塞进 Redux;
- 导致 Redux store 既管理 UI 状态又管理接口数据;
- 造成复杂、臃肿、重复请求。
后来社区逐渐达成共识:
🧭 Client State → 由前端状态库管理 🧭 Server State → 交给 React Query / SWR 等库管理
这样职责清晰、逻辑更简单。
🔧 五、一个直观的对比例子
假设你有一个「用户列表」页面,可以点击展开用户详情弹窗:
xxxxxxxxxx171function UsersPage() {2// server state: 从后端获取的用户列表3const { data: users } = useQuery({4queryKey: ['users'],5queryFn: getUsers,6});78// client state: 控制弹窗开关9const [selectedUser, setSelectedUser] = useState<User | null>(null);1011return (12<>13<UserTable users={users} onSelect={setSelectedUser} />14{selectedUser && <UserModal user={selectedUser} onClose={() => setSelectedUser(null)} />}15</>16);17}🔹
users→ Server State(后端控制) 🔹selectedUser→ Client State(前端控制)💡 六、结论总结
概念 定义 管理方式 常用库 Client State UI 内部、用户交互、表单状态 useState / Context / Redux / Zustand React 内置 Server State 由后端维护,通过 API 获取的数据 React Query / SWR / Apollo TanStack Query ✅ React 负责渲染 UI ✅ Client State 控制交互逻辑 ✅ Server State 控制数据同步
课程内容如下,还是蛮丰富的:

创建项目,如果不想创建项目,老师的github里面提供了react-query-starter的文件夹,可以直接使用。

1、创建:npm create vite@lastest react-query-demo。
2、使用json-server来创建接口,在项目根目录创建db.json文件,从老师的文件把数据拷贝过来。然后创建脚本命令,以后运行npm run json-server就可以启动服务端了。

3、安装react路由npm i react-router,在main.tsx里面添加路由。

创建三个简单的组件/src/components/Home.tsx、src\components\RQSuperHeroes.tsx、src\components\SuperHeroes.tsx,里面写最简单的代码即可。
然后在App.tsx里面编写路由。
xxxxxxxxxx351// App.tsx23import { Routes, Route, Link } from "react-router";4import "./App.css";5import { Home } from "./components/Home";6import { SuperHeroes } from "./components/SuperHeroes";7import { RQSuperHeroes } from "./components/RQSuperHeroes";89function App() {10 return (11 <div>12 <nav>13 <ul>14 <li>15 <Link to="/">Home</Link>16 </li>17 <li>18 <Link to="super-heroes">Tranditional Super Heroes</Link>19 </li>20 <li>21 <Link to="/rq-super-heroes">RQ Super Heroes</Link>22 </li>23 </ul>24 </nav>2526 <Routes>27 <Route path="/super-heroes" element={<SuperHeroes />} />28 <Route path="/rq-super-heroes" element={<RQSuperHeroes />} />29 <Route path="/" element={<Home />} />30 </Routes>31 </div>32 );33}3435export default App;然后按照老师的样式修改App.css和index.css就行了。运行项目:

来看一下使用useState和useEffect来获取数据是什么样的:
安装axios,npm i axios。
xxxxxxxxxx321// src\components\SuperHeroes.tsx23import axios from "axios";4import { useEffect, useState } from "react";56export const SuperHeroes = () => {7 const [isLoading, setIsLoading] = useState(true);8 const [data, setData] = useState<{ name: string }[]>([]);910 useEffect(() => {11 // 这里本来可以直接请求数据的,但是加了一个异步请求延时来演示一下。12 async function getData() {13 await new Promise((resolve) => setTimeout(resolve, 1000));14 axios.get("http://localhost:4000/superheroes").then((res) => {15 setData(res.data);16 setIsLoading(false);17 });18 }1920 getData();21 }, []);2223 if (isLoading) return <h2>Loading...</h2>;24 return (25 <>26 <h2>Super Heroes Page</h2>27 {data.map((hero) => (28 <div key={hero.name}>{hero.name}</div>29 ))}30 </>31 );32};

react-query在2022年停止更新了,文档推荐使用@tanstack/react-query。
安装依赖:npm i @tanstack/react-query。
没想到技术更新这么快,我看一下能不能按照老师的课程标题来使用这个新的库学习,先做着吧。
1、引入Provider,创建client,为应用提供client
xxxxxxxxxx211// App.tsx2345import { QueryClient, QueryClientProvider } from "@tanstack/react-query";67// 创建client8const queryClient = new QueryClient();910function App() {11 return (12 // 为应用提供client13 <QueryClientProvider client={queryClient}>14 <div>15 ......16 </div>17 </QueryClientProvider>18 );19}2021export default App;2、使用useQuery来获取数据
xxxxxxxxxx291// src\components\RQSuperHeroes.tsx23import { useQuery } from "@tanstack/react-query";4import axios from "axios";56export const RQSuperHeroes = () => {7 // useQuery 的返回值有很多,这里使用最简单的isLoading和data8 const { isLoading, data } = useQuery<{ name: string }[]>({9 // 需要提供queryKey,必须是唯一值。这个key可以用来重新请求数据、缓存、共享请求。10 queryKey: ["super-heroes"],11 // queryFn就是请求数据的方法12 queryFn: async () => {13 return await axios.get("http://localhost:4000/superheroes").then((res) => {14 return res.data;15 });16 },17 });1819 if (isLoading) return <h2>Loading...</h2>;2021 return (22 <>23 <h2>RQSuperHeroes Page</h2>24 {data?.map((hero) => (25 <div key={hero.name}>{hero.name}</div>26 ))}27 </>28 );29};
可以将queryFn抽离出来:
xxxxxxxxxx291// src\components\RQSuperHeroes.tsx23import { useQuery } from "@tanstack/react-query";4import axios from "axios";56// 定义请求数据的函数7const fetchSuperHeroes = async (): Promise<{ name: string }[]> => {8 const res = await axios.get("http://localhost:4000/superheroes");9 return res.data;10};1112export const RQSuperHeroes = () => {13 const { isLoading, data } = useQuery<{ name: string }[]>({14 queryKey: ["super-heroes"],15 // queryFn的值就是一个函数16 queryFn: fetchSuperHeroes,17 });1819 if (isLoading) return <h2>Loading...</h2>;2021 return (22 <>23 <h2>RQSuperHeroes Page</h2>24 {data?.map((hero) => (25 <div key={hero.name}>{hero.name}</div>26 ))}27 </>28 );29};定义error状态,然后在请求数据的时候catch,如果有error就展示。

效果:

useQuery的返回值有error和isError,可以使用它们来处理错误。


安装npm i @tanstack/react-query-devtools。
在App.tsx中引入并使用。
xxxxxxxxxx211// App.tsx2345import { QueryClient, QueryClientProvider } from "@tanstack/react-query";6import { ReactQueryDevtools } from "@tanstack/react-query-devtools";78const queryClient = new QueryClient();910function App() {11 return (12 <QueryClientProvider client={queryClient}>13 ......14 15 {/* initialOpen设置是否默认打开,buttonPosition设置打开按钮的位置,默认是bottom-right */}16 <ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-right" />17 </QueryClientProvider>18 );19}2021export default App;可以看到,在页面的右下角,出现了一个button,打开可以看到发起的请求信息。后面会详细介绍这个工具的使用方法。

使用ctrl+shift+r,清除缓存重新加载,然后查看传统请求和useQuery的区别。

我在两个接口请求时,都添加了延时,可以看到传统方式一直显示loading,而useQuery在第一次显示loading之后,就不再显示了,这就是cache缓存的作用。
原理是这样的:当某个queryKey的请求触发之后,isLoading被设置为true,网络请求被发起。当网络请求完成后,react-query会根据queryKey和queryFn将请求结果缓存下来,当重新触发这个请求时,react-query会检查是否存在符合queryKey和queryFn缓存的数据,如果存在,缓存数据会被返回,此时的isLoading不会变为true。react-query知道数据可能会有变化,缓存的数据可能不是最新的,所以在background会触发这个请求,当请求成功后,会在UI上渲染最新的数据。
因为我们的数据没有变化,所以没有看到UI的变化。那有没有东西可以证明background的重新请求发生了呢?有,useQuery返回一个isFetching,可以帮助我们理解。
可以看到,当第一次请求时,isLoading是正常工作的,先是true,请求成功之后变为false。isFetching也是一样。

接下来我们把db.json里面的数据改一下,重新访问这个页面,看一下效果:

可以看到,使用了缓存的数据,数据请求成功之后会更新UI。isLoading一直是false。isFetching先是true,后是false。
react-query缓存的时间默认是300000ms,也就是5分钟。如果想要修改缓存时间,可以向useQuery传入参数gcTime,时间单位是ms。
xxxxxxxxxx61const { isLoading, data, error, isError, isFetching } = useQuery<{ name: string }[], Error>({2 queryKey: ["super-heroes"],3 queryFn: fetchSuperHeroes,4 // 垃圾回收时间5 gcTime: 5000,6});比如说我们设置gcTime: 5000,缓存时间改为5s,看一下效果:

可以看到,5s后这个请求缓存会被垃圾回收处理。
这种缓存做的很好,只在第一次触发loading的显示,后面都是在后台默默的刷新,只要接口返回数据快,我感觉显示效果很好。
stale time:陈旧时间。在数据缓存和Web 开发中非常重要的概念,,它用来定义数据在被认为是“新鲜”的时长。决定了你的应用在多长时间内会信任缓存中的数据,而不会尝试去重新获取新数据。
如果用户可以在一段时间内看缓存的数据,那么我们就没有必要在后台重新请求数据。这时候可以为useQuery设置staleTime参数,设置多长时间内不重新请求数据,单位是ms,默认值为0。
xxxxxxxxxx61const { isLoading, data, error, isError, isFetching } = useQuery<{ name: string }[], Error>({2 queryKey: ["super-heroes"],3 queryFn: fetchSuperHeroes,4 // 保持新鲜的时长5 staleTime: 30000,6});可以看到,第一次请求数据时,network里面有显示,但第二次就没有了,说明在stale time内,不会发起请求。

这节课再来看一些refetch相关的默认设置。
refetchOnMount作用是控制当一个组件“挂载”(Mount)时,是否应该重新触发一次数据请求。默认是true。
组件“挂载”通常发生在:
- 组件第一次被渲染到屏幕上时。
- 用户离开一个页面,然后又返回到这个页面时(导致组件被卸载后又重新挂载)。
它的值有true | false | "always",当设置为always时,当组件挂载时,总是重新获取数据。它会无视 staleTime。不管数据是不是“新鲜”的 (fresh),只要组件一挂载,就立刻强制发起一次新的网络请求。
refetchOnWindowFocus作用:当浏览器窗口或标签页重新获得焦点(Focus)时,检查并重新获取已挂载查询的数据。
值有true | false | "always"。当设置为always时,强制重新获取窗口聚焦时,无视 staleTime,总是触发重新获取。
场景举例
想象一个用户正在使用你的应用,然后在后台打开了一个新的标签页,或者切换到了另一个程序,几分钟后用户又:
- 点击 了你的浏览器标签页。
- Alt + Tab 切换回了你的应用程序窗口。
- 点击 了页面上的空白区域使页面重新聚焦。
一旦发生上述操作,如果设置了
refetchOnWindowFocus,React Query就会在后台自动触发一次数据请求,将当前页面上的旧数据更新为最新数据。
看一下传统数据请求在用户切换窗口之后,会不会重新请求数据。我先打开Traditional Super Heroes页面,然后在db.json里面修改数据,看会不会重新获取数据。
可以看到,没有。

react query里面的表现,可以看到重新请求了数据。

Polling (轮询) 是一种非常基础且常见的网络通信技术,用于在客户端(如浏览器、App)和服务器之间同步数据或检查状态。轮询就是客户端每隔固定的时间间隔,主动向服务器发送请求,询问“是否有新数据或新状态?”。
比如说我展示一个股票的界面,需要隔一段时间显示最新的数据,这时候的需求就是不断请求数据。
那在react query里面怎么做到呢?可以设置refetchInterval参数,默认是false,如果设置为数字,单位为ms。
xxxxxxxxxx61const { isLoading, data, error, isError, isFetching } = useQuery<{ name: string }[], Error>({2 queryKey: ["super-heroes"],3 queryFn: fetchSuperHeroes,4 // 设置重新请求的时间间隔5 refetchInterval: 2000,6});可以看到,每隔2s,会重新请求数据。

当浏览器没有focus的时候,即使设置了refetchInterval,请求也会中断。如果想在浏览器没有focus的时候,依然请求数据,这时候可以设置refetchIntervalInBackground: true。
题外话
除了轮询,还有什么技术?
技术 方式 实时性 典型场景 Polling (轮询) 客户端主动问:“好了吗?” (每 N 秒问一次) 低 简单的状态检查,或低实时性要求。 Long Polling (长轮询) 客户端问:“好了吗?” 服务器先不回,直到有数据或超时才回。 中高 在 WebSocket 不支持时的替代方案。 WebSockets 双向持久连接,服务器随时可以推送数据。 极高 (实时) 实时聊天、多人游戏、股票行情。 Server-Sent Events (SSE) 单向持久连接,服务器可以主动向客户端推送数据。 高 新闻推送、实时更新的仪表盘。
之前触发网络请求,要么是组件onMount时、要么是window focus时。如果我想在点击某个元素的时候触发网络请求,怎么做呢?
1、设置useQuery不要再组件onMount时请求数据,使用enabled参数
xxxxxxxxxx61const { isLoading, data, error, isError, isFetching } = useQuery<{ name: string }[], Error>({2 queryKey: ["super-heroes"],3 queryFn: fetchSuperHeroes,4 // 设置不要在组件onMount的时候发起网络请求5 enabled: false,6 });2、使用useQuery的返回值refetch,可以手动触发网络请求
xxxxxxxxxx371// components\RQSuperHeroes.tsx23import { useQuery } from "@tanstack/react-query";4import axios from "axios";56const fetchSuperHeroes = async (): Promise<{ name: string }[]> => {7 await new Promise((resolve) => setTimeout(resolve, 1000));8 const res = await axios.get("http://localhost:4000/superheroes");9 return res.data;10};1112export const RQSuperHeroes = () => {13 // 解构出 refetch 方法14 const { isLoading, data, error, isError, isFetching, refetch } = useQuery<{ name: string }[], Error>({15 queryKey: ["super-heroes"],16 queryFn: fetchSuperHeroes,17 // 设置不要在组件onMount的时候发起网络请求18 enabled: false,19 });2021 console.log({ isLoading, isFetching });2223 if (isLoading) return <h2>Loading...</h2>;2425 if (isError) return <h2>{error.message}</h2>;2627 return (28 <>29 <h2>RQSuperHeroes Page</h2>30 {/* 设置手动触发网络请求 */}31 <button onClick={() => refetch()}>Fetch Heroes</button>32 {data?.map((hero) => (33 <div key={hero.name}>{hero.name}</div>34 ))}35 </>36 );37};可以看到,手动触发之后才发起请求。并且staleTime和cache也是同样工作的。

如果想在再次手动触发的时候显示loading效果,可以使用isFetching来帮忙。
xxxxxxxxxx11if (isLoading || isFetching) return <h2>Loading...</h2>;在数据请求之后,我们可能想做一些副作用,比如说打开一个modal、跳转到一个新路由、或者显示toast提示信息。
原来的做法:

但是在@tanstack/react-queryv5里面的useQuery里面的onSuccess和onError已经移除了,但在useMutation里面还保留着。
因为这些回调不会在数据从缓存中直接读取时触发。比如数据被缓存后,再次同 key 请求可能直接走缓存,不会触发
onSuccess。
那这里该怎么写呢?我觉得onSuccess可以直接使用isSuccess来代替,而onError可以用isError和error来代替,结合useEffect来使用。
xxxxxxxxxx411import { useQuery } from "@tanstack/react-query";2import axios from "axios";3import { useEffect } from "react";45const fetchSuperHeroes = async (): Promise<{ name: string }[]> => {6 await new Promise((resolve) => setTimeout(resolve, 1000));7 const res = await axios.get("http://localhost:4000/superheroes");8 return res.data;9};1011export const RQSuperHeroes = () => {12 // 解构出 isSuccess isError error13 const { isLoading, data, error, isError, isFetching, isSuccess } = useQuery<{ name: string }[], Error>({14 queryKey: ["super-heroes"],15 queryFn: fetchSuperHeroes,16 });171819 useEffect(() => {20 if (isSuccess) {21 console.log("Perform side effect after data fetching");22 }23 if (isError) {24 console.log("Perform side effct after encountering error", error.message);25 }26 }, [isSuccess, isError, error]);2728 29 if (isLoading || isFetching) return <h2>Loading...</h2>;3031 if (isError) return <h2>{error.message}</h2>;3233 return (34 <>35 <h2>RQSuperHeroes Page</h2>36 {data?.map((hero) => (37 <div key={hero.name}>{hero.name}</div>38 ))}39 </>40 );41};先看请求成功,然后修改请求地址看请求失败的情况:

可以看到,请求失败之后,会重试3次,3次再失败才会变更isError的值。
这节课学习转变接口返回数据的格式。
可以使用useQuery的select参数,传递一个函数进去,这个函数可以接收到接口返回的数据。这时候useQuery的ts类型,需要传递第3个泛型进去。
假设需求是:将superheroes的返回结果改为字符串数组,这样就不需要使用hero.name来访问了。
xxxxxxxxxx341// RQSuperHeroes.tsx23import { useQuery } from "@tanstack/react-query";4import axios from "axios";5import { useEffect } from "react";67const fetchSuperHeroes = async (): Promise<{ name: string }[]> => {8 await new Promise((resolve) => setTimeout(resolve, 1000));9 const res = await axios.get("http://localhost:4000/superheroes1");10 return res.data;11};1213export const RQSuperHeroes = () => {14 // useQuery 的ts类型,要传递第三个泛型参数进去,这表示select之后返回结果的类型15 const { isLoading, data, error, isError, isFetching, isSuccess } = useQuery<{ name: string }[], Error, string[]>({16 queryKey: ["super-heroes"],17 queryFn: fetchSuperHeroes,18 // select参数的值是一个函数,函数可以接收到接口返回的数据,然后做处理即可19 select: (data) => {20 return data.map((hero) => hero.name);21 },22 });2324 2526 return (27 <>28 <h2>RQSuperHeroes Page HeroNames</h2>29 {data?.map((heroName) => (30 <div key={heroName}>{heroName}</div>31 ))}32 </>33 );34};
这节课学习怎么包裹useQuery这个hook,创建自定义的hook,这样在不同的组件里面,我们就可以使用自定义hook来调用接口了。
1、创建hook
因为hook就是一个函数,我们要的最终返回结果是什么呢?其实就是useQuery的调用过程,这一点是最重要的,要搞清楚。
xxxxxxxxxx221// src\hooks\useSuperHeroesData.ts23import { useQuery } from "@tanstack/react-query";4import axios from "axios";56const fetchSuperHeroes = async (): Promise<{ name: string }[]> => {7 await new Promise((resolve) => setTimeout(resolve, 1000));8 const res = await axios.get("http://localhost:4000/superheroes");9 return res.data;10};1112export const useSuperHeroesData = () => {13 return useQuery<{ name: string }[], Error, string[]>({14 // 需要提供queryKey,必须是唯一值。这个key可以用来重新请求数据、缓存、共享请求。15 queryKey: ["super-heroes"],16 // queryFn就是请求数据的方法17 queryFn: fetchSuperHeroes,18 select: (data) => {19 return data.map((hero) => hero.name);20 },21 });22}注意:自定义hook中可以设置参数,然后用到useQuery里面就行了。这就是函数柯里化。
2、然后在不同的组件里面使用自定义hook即可
xxxxxxxxxx311// src\components\RQSuperHeroes.tsx23import { useEffect } from "react";4import { useSuperHeroesData } from "../hooks/useSuperHeroesData";56export const RQSuperHeroes = () => {7 // 只需要调用 useSuperHeroesData 即可8 const { isLoading, data, error, isError, isFetching, isSuccess } = useSuperHeroesData();910 useEffect(() => {11 if (isSuccess) {12 console.log("Perform side effect after data fetching");13 }14 if (isError) {15 console.log("Perform side effct after encountering error", error.message);16 }17 }, [isSuccess, isError, error]);1819 if (isLoading || isFetching) return <h2>Loading...</h2>;2021 if (isError) return <h2>{error.message}</h2>;2223 return (24 <>25 <h2>RQSuperHeroes Page HeroNames</h2>26 {data?.map((heroName) => (27 <div key={heroName}>{heroName}</div>28 ))}29 </>30 );31};工作正常:

这节课学习怎么使用id来查询详情数据。需要做下面的事情:

1、创建RQSuperHeroDetail.tsx文件
xxxxxxxxxx51// src\components\RQSuperHeroDetail.tsx23export const RQSuperHeroDetail = () => {4 return <div>RQSuperHeroDetail</div>;5};2、在App.tsx里面添加路由,在RQSuperHeroes页面添加跳转链接

xxxxxxxxxx71// src\components\RQSuperHeroes.tsx23{data?.map((hero) => (4 <div key={hero.id}>5 <Link to={`/rq-super-heroes/${hero.id}`}>{hero.name}</Link>6 </div>7))}可以看到,跳转正常:

3、编写自定义hook,获取详情数据
xxxxxxxxxx251// src\hooks\useSuperHeroDetail.ts23import { useQuery } from "@tanstack/react-query";4import axios from "axios";56type User = {7 name: string;8 id: number;9 alterEgo: string;10}1112// 接口请求的函数13const fetchSuperHero = async (id: string): Promise<User> => {14 const res = await axios.get("http://localhost:4000/superheroes/" + id)15 return res.data;16}1718// 自定义Hook19export const useSuperHeroDetail = (id: string) => {20 return useQuery({21 // 这里的queryKey需要多传递一个id,因为id值不同,缓存也不同22 queryKey: ["super-hero", id],23 queryFn: () => fetchSuperHero(id)24 })25}4、在页面中使用自定义hook获取数据
xxxxxxxxxx191// src\components\RQSuperHeroDetail.tsx23import { useParams } from "react-router";4import { useSuperHeroDetail } from "../hooks/useSuperHeroDetail";56export const RQSuperHeroDetail = () => {7 // 利用react-router的方法,获取params参数8 const { heroId } = useParams();9 const { isLoading, data, isError, error } = useSuperHeroDetail(heroId || "");1011 if (isLoading) return <h2>Loading...</h2>;12 if (isError) return <h2>{error.message}</h2>;1314 return (15 <div>16 {data?.name} - {data?.alterEgo}17 </div>18 );19};可以看到,获取了详情数据。

注意:在指定queryFn的函数时,可以不传递参数,useQuery会将参数传递过来,里面有一个querykey的参数,是一个数据,获取它的第二个参数即可。


有时候,一个组件里面需要调用多个API接口来获取需要的数据,多个接口之间没有相互关系,所以称它们为平行请求。
创建新组件:
xxxxxxxxxx51// src\components\ParallelQueries.tsx23export const ParallelQueries = () => {4 return <div>ParallelQueries</div>;5};App.tsx里面添加新路由<Route path="/rq-parallel" element={<ParallelQueries />} />。
需求就是在新组件里面,请求superheroes和friends的数据。
xxxxxxxxxx431// src\components\ParallelQueries.tsx23import { useQuery } from "@tanstack/react-query";4import axios from "axios";56const fetchSuperHeroes = async (): Promise<{ id: string; name: string; alterEgo: string }[]> => {7 const res = await axios.get("http://localhost:4000/superheroes");8 return res.data;9};1011const fetchFriends = async (): Promise<{ id: string; name: string }[]> => {12 const res = await axios.get("http://localhost:4000/frieds");13 return res.data;14};1516export const ParallelQueries = () => {17 // 只需要写两个useQuery就行了18 const { data: superHeroes } = useQuery({19 queryKey: ["super-heroes"],20 queryFn: fetchSuperHeroes,21 });22 const { data: friends } = useQuery({23 queryKey: ["friends"],24 queryFn: fetchFriends,25 });26 return (27 <>28 <h2>Super Heroes</h2>29 {superHeroes?.map((hero) => (30 <div key={hero.id}>31 {hero.id} - {hero.name} - {hero.alterEgo}32 </div>33 ))}3435 <h2>Friends</h2>36 {friends?.map((f) => (37 <div key={f.id}>38 {f.id} - {f.name}39 </div>40 ))}41 </>42 );43};
动态平行查询。
案例:我DynamicParallel页面,请求多个superhero的详情数据,传递的heroIds的长度是未知的。所以不能像上节课那样,使用多个useQuery来解决问题,而是需要使用useQueries。
添加路由:
xxxxxxxxxx11<Route path="/rq-dynamic-parallel" element={<DynamicParallel heroIds={[1, 3]} />} />编写组件:
xxxxxxxxxx361// src\components\DynamicParallel.tsx23import { useQueries, UseQueryResult } from "@tanstack/react-query";4import axios from "axios";56const fetchSuperHeroData = async (id: number): Promise<{ id: string; name: string }> => {7 const res = await axios.get(`http://localhost:4000/superheroes/${id}`);8 return res.data;9};1011export const DynamicParallel = ({ heroIds }: { heroIds: number[] }) => {12 // 使用 useQueries 的queries参数,参数值是数组。这里是了数组的map方法,返回的是数组13 const results = useQueries({14 queries: heroIds.map((item) => ({15 queryKey: ["super-hero", item],16 queryFn: () => fetchSuperHeroData(item),17 })),18 }) as UseQueryResult<{ id: string; name: string }>[];1920 if (results.length === 0) return <h2></h2>;2122 return (23 <div>24 {results.map((c) => {25 // 只渲染成功的数据26 if (!c.data) return null;2728 return (29 <div key={c.data?.id}>30 {c.data?.id} - {c.data?.name}31 </div>32 );33 })}34 </div>35 );36};可以看到,两个请求发起了。

如果多个请求返回的结果结构类似,还可以使用combine方法,将多个返回结果合并成一个:

实际应用场景:
一个组件中需要同时加载多个接口的数据。比如:
- 用户信息
/api/user/1- 用户的文章
/api/user/1/posts- 用户的好友列表
/api/user/1/friends那么就可以写成这样:
xxxxxxxxxx71const results = useQueries({2queries: [3{ queryKey: ['user', id], queryFn: fetchUser },4{ queryKey: ['posts', id], queryFn: fetchPosts },5{ queryKey: ['friends', id], queryFn: fetchFriends },6],7})
results是一个数组:xxxxxxxxxx51[2{ data, isLoading, error, }, // user3{ data, isLoading, error, }, // posts4{ data, isLoading, error, } // friends5]
有时候,我们需要按照顺序请求数据,下一个请求的参数要依靠上一个请求的返回结果。
在db.json里面新增数据:

需求:先根据email获取user信息,然后根据channelId获取courses信息。
1、添加路由:
xxxxxxxxxx11<Route path="/rq-dependent" element={<DependentQueries email="vishwas@example.com" />} />2、获取数据
xxxxxxxxxx381// src\components\DependentQueries.tsx23import { useQuery } from "@tanstack/react-query";4import axios from "axios";56const fetchUserByEmail = async (email: string): Promise<{ channelId: string }> => {7 const res = await axios.get(`http://localhost:4000/users/${email}`);8 return res.data;9};1011const fetchCoursesByChannelId = async (channelId: string): Promise<string[]> => {12 const res = await axios.get(`http://localhost:4000/channels/${channelId}`);13 return res.data;14};1516export const DependentQueries = ({ email }: { email: string }) => {17 const { data: user } = useQuery({18 queryKey: ["user", email],19 queryFn: () => fetchUserByEmail(email),20 });2122 const channelId = user?.channelId || "";2324 const { data: courses } = useQuery({25 queryKey: ["courses", channelId],26 queryFn: () => fetchCoursesByChannelId(channelId),27 // enabled: !!channelId 是核心,它会让查询完全不发起,直到条件满足。28 enabled: !!channelId,29 });3031 return (32 <div>33 {courses?.map((c) => (34 <div key={c}>{c}</div>35 ))}36 </div>37 );38};还是使用两个useQuery,核心是:在第二个请求中,设置enabled: !!channelId,这样当channelId没有值时,第二个请求是不会发起的。

这两个接口的执行流程到底是什么呢?
执行流程全过程(从组件 mount 开始,每一毫秒都说清楚,下面的时间ms都是模拟说明的文字)
| 时间点 | 发生了什么 | query1 状态 | query2 状态 | 网络请求 | 控制台会打印什么(如果你加了 console.log) |
|---|---|---|---|---|---|
| 0ms 组件刚 mount | React Query 创建两个 query 对象,但 只有 enabled=true 的才会启动 | status: 'loading' isFetching: true | status: 'idle' isFetching: false | 只发 接口1 的请求 | query1: loading query2: idle(完全不存在) |
| 100ms 接口1 还在请求中 | query1 正在 pending,query2 看到 query1.data 是 undefined → !!undefined = false | loading | idle(不会创建 observer) | 只有接口1在飞 | query2 enabled=false,不发请求 |
| 800ms 接口1 请求成功返回 | query1.data = { id: 42 } React 重新 render → !!query1.data?.id 从 false 变成 true | status: 'success' data: {id:42} | 瞬间从 idle → loading React Query 自动创建 observer 并执行 queryFn | 立刻发接口2 的请求(零延迟) | query1: success query2: 条件满足!从 idle → loading,发起 fetchUserDetail(42) |
| 850ms 接口2 请求中 | query1 已成功,query2 正在 pending | success | loading isFetching: true | 接口2 在飞 | query2: 正在获取详情... |
| 1500ms 接口2 成功返回 | query2.data = { name: 'Grok', level: 99 } | success | status: 'success' isFetching: false | 两个请求都完成 | query2: success,拿到数据啦! |
| 之后 组件 rerender 或页面刷新 | 由于 queryKey 已缓存,两个接口都不会再发请求(除非 staleTime 到期) | success (from cache) | success (from cache) | 零请求 | query1: 从缓存读取 query2: enabled=true,直接从缓存读 |
你只管写 enabled: !!dep,React Query 帮你处理了“创建/销毁/订阅/缓存/重试”所有脏活!
每个 useQuery 钩子 = React Query 在内部 new QueryObserver() 一个实例,它负责:
这节课学习怎么为query请求提供初始的查询数据,当从superheroes列表访问详情的时候,我们可以使用列表里面的一些数据,提供给详情界面先显示,然后里面请求更具体的详情数据。
1、使用queryClient的getQueryData方法,拿到列表的数据

2、使用拿到的数据,设置useQuery的initialData的值
xxxxxxxxxx321// src\hooks\useSuperHeroDetail.ts23import { useQuery, useQueryClient } from "@tanstack/react-query";4import axios from "axios";56type User = {7 name: string;8 id: string;9 alterEgo: string;10}1112// 接口请求的函数13const fetchSuperHero = async ({ queryKey }: { queryKey: [string, string] }): Promise<User> => {14 const [, id] = queryKey;15 const res = await axios.get("http://localhost:4000/superheroes/" + id)16 return res.data;17}1819// 自定义Hook20export const useSuperHeroDetail = (id: string) => {21 const queryClient = useQueryClient();22 return useQuery({23 queryKey: ["super-hero", id],24 queryFn: fetchSuperHero,25 // 使用initialData赋初始值,然后使用queryClien获取列表数据26 initialData: () => {27 const heroes = queryClient.getQueryData<User[]>(["super-heroes"]);28 const hero = heroes?.find(hero => hero.id === id);29 return hero ?? undefined;30 }31 })32}可以看到,数据显示了,但是network里面过了一段时间才发起请求,说明初始值成功赋值。

这节课学习分页请求。
1、添加路由:
xxxxxxxxxx11<Route path="/rq-paginated" element={<PaginatedQueriesPage />} />2、编写组件
json server的分页这样写:http://localhost:4000/colors?_page=2&_per_page=2。
思路是创建一个页码的状态,把这个状态传递给请求数据的函数。当这个状态变化时,组件会重新渲染,那么useQuery会接收到最新的页码来请求数据。流程并不复杂。
xxxxxxxxxx471// src\components\PaginatedQueries.tsx23import { useQuery } from "@tanstack/react-query";4import axios from "axios";5import { useState } from "react";67const fetchColors = async (8 pageNum: number9): Promise<{10 data: { id: string; label: string }[];11}> => {12 const res = await axios.get(`http://localhost:4000/colors?_page=${pageNum}&_per_page=2`);13 return res.data;14};1516export const PaginatedQueriesPage = () => {17 const [pageNumber, setPageNumber] = useState(1);18 const { isLoading, isError, error, data } = useQuery({19 queryKey: ["colors", pageNumber],20 queryFn: () => fetchColors(pageNumber),21 });2223 if (isLoading) return <h2>Loading...</h2>;24 if (isError) return <h2>{error.message}</h2>;2526 return (27 <>28 <div>29 {data?.data?.map((color) => (30 <div key={color.id}>31 <h2>32 {color.id}. {color.label}33 </h2>34 </div>35 ))}36 </div>37 <div>38 <button onClick={() => setPageNumber((page) => page - 1)} disabled={pageNumber === 1}>39 Prev Page40 </button>41 <button onClick={() => setPageNumber((page) => page + 1)} disabled={pageNumber === 4}>42 Next Page43 </button>44 </div>45 </>46 );47};可以看到,分页效果很好。

但是有个问题,就是每次请求新数据的时候,都有Loading,能不能不显示loading,而是显示之前的数据,可以使用placeholderData属性。(我觉得显示loading才是正常的,显示之前的数据可能让用户觉得点击按钮不起作用,会一直点击)

xxxxxxxxxx51const { isLoading, isError, error, data } = useQuery({2 queryKey: ["colors", pageNumber],3 queryFn: () => fetchColors(pageNumber),4 placeholderData: (previousData) => previousData,5});可以看到,向前翻页的时候,没有显示Loading。

适用于“无限滚动”的场景。
需求:在colors下面添加一个load more的按钮,每次点击这个按钮,会新增显示两个colors。
1、添加路由
xxxxxxxxxx11<Route path="/rq-infinite" element={<InfiniteQueriesPage />} />2、编写组件,使用useInfiniteQuery来实现“无限滚动”的场景
xxxxxxxxxx681// 23import { useInfiniteQuery } from "@tanstack/react-query";4import axios from "axios";5import React from "react";67interface Color {8 id: number;9 label: string;10}1112interface ColorsResponse {13 data: Color[];14 next: number;15 pages: number;16}1718// 请求函数,pageParam这个参数是useInfiniteQuery传递过来的。19const fetchColors = async ({ pageParam = 1 }): Promise<ColorsResponse> => {20 const res = await axios.get(`http://localhost:4000/colors?_page=${pageParam}&_per_page=2`);21 return res.data;22};2324export const InfiniteQueriesPage = () => {25 const { isLoading, isError, error, data, fetchNextPage, isFetching, hasNextPage } = useInfiniteQuery({26 queryKey: ["colors"],27 queryFn: fetchColors,28 // 👇 告诉 React Query 如何获取下一页参数29 getNextPageParam: (lastPage) => {30 // 这里lastPage里面具体有哪些属性,要看后端返回值是什么31 if (lastPage.next <= lastPage.pages) {32 return lastPage.next;33 }34 return undefined; // ❌ 没有更多页时返回 undefined35 },3637 initialPageParam: 1, // ✅ v5 必须显式指定38 });3940 if (isLoading) return <h2>Loading...</h2>;41 if (isError) return <h2>{error.message}</h2>;4243 console.log(data, hasNextPage);4445 return (46 <>47 <div>48 {data?.pages.map((group, i) => (49 <React.Fragment key={i}>50 {group?.data.map((color) => (51 <div key={color.id}>52 <h2>53 {color.id}. {color.label}54 </h2>55 </div>56 ))}57 </React.Fragment>58 ))}59 </div>60 <div>61 <button disabled={!hasNextPage} onClick={() => fetchNextPage()}>62 Load More63 </button>64 </div>65 <div>{isFetching ? "Fetching..." : null}</div>66 </>67 );68};
前面20节课,讲了获取数据,接下来讲解提交post数据。
在react query中,mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, TanStack Query exports a useMutation hook.
1、创建自定义提交hook
xxxxxxxxxx241// 23import { useMutation, useQuery } from "@tanstack/react-query";4import axios from "axios";56type User = {7 name: string;8 id: string;9 alterEgo: string;10}11121314// 接收mutationFn传递过来的参数,直接发起post请求即可15const addSuperHero = async (hero: Omit<User, "id">) => {16 return await axios.post("http://localhost:4000/superheroes", hero);17}1819// mutationFn会传递参数到数据请求函数里面去,所以它可以接收到20export const useAddSuperHero = () => {21 return useMutation({22 mutationFn: addSuperHero23 })24}2、在RQSuperHeroes页面创建提交UI,调用自定义提交HOOK
xxxxxxxxxx541// src\components\RQSuperHeroes.tsx23import { useEffect, useState } from "react";4import { useSuperHeroesData, useAddSuperHero } from "../hooks/useSuperHeroesData";5import { Link } from "react-router";67export const RQSuperHeroes = () => {8 // 创建两个输入框状态9 const [name, setName] = useState("");10 const [alterEgo, setAlterEgo] = useState("");1112 const { isLoading, data, error, isError, isFetching, refetch, isSuccess } = useSuperHeroesData();1314 // useMutation返回一个mutate方法,调用这个方法可以发起请求15 const { mutate } = useAddSuperHero();1617 const handleAddHeroClick = () => {18 const hero = {19 name,20 alterEgo,21 };22 mutate(hero);23 };2425 useEffect(() => {26 if (isSuccess) {27 console.log("Perform side effect after data fetching");28 }29 if (isError) {30 console.log("Perform side effct after encountering error", error.message);31 }32 }, [isSuccess, isError, error]);3334 if (isLoading) return <h2>Loading...</h2>;3536 if (isError) return <h2>{error.message}</h2>;3738 return (39 <>40 <h2>RQSuperHeroes Page HeroNames</h2>41 <div>42 <input type="text" value={name} onChange={(e) => setName(e.target.value)} />43 <input type="text" value={alterEgo} onChange={(e) => setAlterEgo(e.target.value)} />44 <button onClick={handleAddHeroClick}>Add Hero</button>45 </div>46 <button onClick={() => refetch()}>Fetch Heroes</button>47 {data?.map((hero) => (48 <div key={hero.id}>49 <Link to={`/rq-super-heroes/${hero.id}`}>{hero.name}</Link>50 </div>51 ))}52 </>53 );54};可以看到,新增成功之后,手动refetch,就可以看到数据真的添加了。

我自己写的Add button事件的代码:
xxxxxxxxxx111const handleAddHeroClick = () => {2const hero = {3name,4alterEgo,5};6const res = mutate(hero);7console.log({ res });8if (typeof res === "object" && Object.keys(res).length > 0) {9refetch();10}11};我通过判断返回值是否正常,来手动refetch,并没有更新list数据,为什么?
核心原因是:
mutate()并 不会 立即返回结果,也不会返回一个 Promise 或服务器响应。这里的res实际上是undefined。怎么做呢?
要么在useMutation里面加onSuccess回调,使用下节课会学习到的query invalidation。
在新增成功之后,能不能自动refetch呢?可以,需要使用Invalidations from mutations这个特性。在mutation的onSuccess事件里面添加useQueryClient的invalidateQueries方法。

invalidation:失效。让查询失效。
这个字面意思很难理解。因为这涉及到react-query的执行机制。简单点说,就是:让某个缓存的数据“失效(invalidate)”,从而触发重新获取(refetch)。
x
1// src\hooks\useSuperHeroesData.ts23import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";4import axios from "axios";56type User = {7 name: string;8 id: string;9 alterEgo: string;10}11121314const addSuperHero = async (hero: Omit<User, "id">) => {15 const res = await axios.post("http://localhost:4000/superheroes", hero)16 return res;17}1819export const useAddSuperHero = () => {20 const queryClient = useQueryClient();21 return useMutation({22 mutationFn: addSuperHero,23 onSuccess: () => {24 queryClient.invalidateQueries({25 queryKey: ["super-heroes"]26 })27 }28 })29}可以看到新增之后,列表数据自动更新了。

执行过程如下:
- 用户添加一个新英雄;
mutation调用成功;- 执行
invalidateQueries({queryKey: ["super-heroes"]});使这个查询缓存的结果失效。- React Query 把这个 key 的缓存标记为「stale(过期)」;
- 如果有组件正在使用这个数据(比如列表页),React Query 会自动 重新发请求;
- UI 自动更新 ✅。
因为新增成功之后的返回结果是新增的对象,所以可以拿到这个对象直接添加到列表缓存数据里面去,节省一次网络请求。
x
1// 23import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";4import axios from "axios";5678const addSuperHero = async (hero: Omit<User, "id">) => {9 const res = await axios.post("http://localhost:4000/superheroes", hero)10 return res;11}1213export const useAddSuperHero = () => {14 const queryClient = useQueryClient();15 return useMutation({16 mutationFn: addSuperHero,17 onSuccess: (data) => {18 // 使用 setQueryData ,为缓存的结果添加一个数据。19 queryClient.setQueryData(["super-heroes"], (oldHeroData: User[]) => {20 return [oldHeroData, data.data]21 })22 }23 })24}可以看到,在新增之后,没有发起列表查询请求,但是列表更新了。

乐观更新。
💬 定义:乐观更新是一种用户体验优化技巧,意思是: 在服务器还没返回成功之前,就先在 UI 上“假设成功”,直接更新界面。如果请求失败,再“回滚”到旧状态。
通常流程是这样的:
| 普通模式 | 乐观更新模式 |
|---|---|
| 1️⃣ 用户点击按钮 | 1️⃣ 用户点击按钮 |
| 2️⃣ 显示 loading... | 2️⃣ 立即把 “Thor” 加到界面上(假装成功) |
| 3️⃣ 请求成功后再更新 | 3️⃣ 请求成功时保持现状(虽然onSettled里面会重新请求数据,但是我们在onMutate里面添加的数据和后端的数据在页面上展示的内容应该是一致的,所以UI不会有变化) |
| 4️⃣ 请求失败再提示错误 | 4️⃣ 请求失败 → 回滚 UI(删除 “Thor”) |
22、23两节课学习了在新增成功之后再更新列表数据,这节课来学习新增点击之后就理解更新UI,然后再发post请求,post请求完成后发起列表get请求。
101用户点击按钮2 ↓3onMutate() → 暂停旧请求 + 保存旧数据 + 更新缓存(UI立即变)4 ↓5服务器请求发出(异步)6 ↓7成功 → invalidateQueries() → 真正刷新数据8 ↓9失败 → onError() → 回滚缓存(恢复旧UI)10x
1// src\hooks\useSuperHeroesData.ts23import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";4import axios from "axios";56type User = {7 name: string;8 id: string;9 alterEgo: string;10}11121314const addSuperHero = async (hero: Omit<User, "id">) => {15 const res = await axios.post("http://localhost:4000/superheroes", hero)16 return res;17}1819export const useAddSuperHero = () => {20 const queryClient = useQueryClient();21 return useMutation({22 mutationFn: addSuperHero,23 // onMutate 在请求发起之前触发,会接收到请求数据24 onMutate: async (newHero) => {25 // 暂停旧请求26 await queryClient.cancelQueries({27 queryKey: ["super-heroes"]28 })2930 // 获取旧数据31 const prevHeroData = queryClient.getQueryData(["super-heroes"]);3233 // 先更新缓存(让UI立即看到变化)34 queryClient.setQueryData(["super-heroes"], (oldHeroData: User[]) => {35 return [oldHeroData, {36 // 这里的id要根据实际情况来37 id: oldHeroData.length + 1,38 newHero39 }]40 })4142 // 返回一个回滚函数的上下文43 return {44 prevHeroData45 }46 },47 // 如果请求失败,则回滚缓存48 onError: (_err, _newHero, context) => {49 if (context?.prevHeroData) {50 queryClient.setQueryData(['super-heroes'], context.prevHeroData)51 }52 },53 // 不管请求是否成功,都会重新同步服务器数据54 onSettled: () => {55 queryClient.invalidateQueries({56 queryKey: ["super-heroes"]57 })58 }59 })60}可以看到,界面更新了之后,列表请求才发起,说明速度还是非常快的。

我把post请求的地址修改一下,看一下请求错误的情况。
gif可能看不到,但是我在录制的时候看到了,列表数据显示闪了一下显示新增的数据,但是因为接口报错,所有回滚了数据,之后重新发起了列表数据请求。

乐观更新使用场景:
| 场景 | 示例 |
|---|---|
| ✅ 新增数据 | 添加评论、添加任务、添加列表项 |
| ✅ 删除数据 | 删除评论、删除 todo |
| ✅ 点赞 / 收藏 | 点赞按钮立刻变亮 |
| ✅ 切换状态 | 开关、切换模式 |
await queryClient.cancelQueries({ queryKey: ['super-heroes'] });,为什么要加上这一句?1、告诉 React Query:别再让
['super-heroes']的请求去更新缓存;2、等当前 mutation 完成后,我们再重新请求(
invalidateQueries)。
这节课学习怎么封装axios请求。
x
1// react-query-demo\src\utils\axios-util.ts23import axios from "axios";45const client = axios.create({6 baseURL: "http://localhost:4000"7})89export const request = ({ options }) => {10 client.defaults.headers.common.Authorization = "Bearer token"1112 const onSuccess = (response) => response;13 const onError = (error) => {14 // 这里可以写错误处理的代码15 return error16 }1718 return client(options).then(onSuccess).catch(onError)19}在组件里面使用看一下:
x
1// src\hooks\useSuperHeroesData.ts23import { request } from "../utils/axios-util";45type User = {6 name: string;7 id: string;8 alterEgo: string;9}1011const fetchSuperHeroes = async (): Promise<User[]> => {12 await new Promise((resolve) => setTimeout(resolve, 1000));13 const res = await request({ url: "/superheroes" });14 return res.data;15};1617const addSuperHero = async (hero: Omit<User, "id">) => {18 const res = await request({ url: "/superheroes", method: "post", data: hero })19 return res;20}2122使用正常:
